#define _GNU_SOURCE /* asprintf */

#include <err.h>
#include <time.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <paths.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <getopt.h>
#include <inttypes.h>
#include <sys/time.h>
#include <sys/stat.h>

#include <metalang99.h>

#include "../util.h"
#include "../getopt_helper.h"

enum {
	SUFFIX_OPTION = GETOPT_LONG_ENUM_INIT,
};

static struct option const LONG_OPTIONS[] = {
	{ "directory", no_argument, NULL, 'd' },
	{ "quiet", no_argument, NULL, 'q' },
	{ "dry-run", no_argument, NULL, 'u' },
	{ "suffix", required_argument, NULL, SUFFIX_OPTION },
	{ "tmpdir", optional_argument, NULL, 'p' },
	GETOPT_END_DECL
};

#define NFORMAT 2
static const char usage_str[] =
    "Usage: %s [OPTION]... [TEMPLATE]\n"
    "Create a temporary file or directory, safely, and print its name.\n"
    "TEMPLATE must contain at least 3 consecutive 'X's in last component.\n"
    "If TEMPLATE is not specified, use tmp.XXXXXXXXXX, and --tmpdir is "
    "implied.\n"
    "Files are created u+rw, and directories u+rwx, minus umask "
    "restrictions.\n"
    "\n"
    "  -d, --directory        create a directory, not a file\n"
    "  -u, --dry-run          do not create anything; merely print a name "
    "(unsafe)\n"
    "  -q, --quiet            suppress diagnostics about file/dir-creation "
    "failure\n"
    "      --suffix=SUFF      append SUFF to TEMPLATE; SUFF must not contain "
    "a slash.\n"
    "                         This option is implied if TEMPLATE does not end "
    "in X\n\n"
    "  -p DIR, --tmpdir[=DIR] interpret TEMPLATE relative to DIR; if DIR is "
    "not\n"
    "                         specified, use $TMPDIR if set, else /tmp.  "
    "With\n"
    "                         this option, TEMPLATE must not be an absolute "
    "name;\n"
    "                         unlike with -t, TEMPLATE may contain slashes, "
    "but\n"
    "                         %s creates only the final component\n\n"
    "  -t                     interpret TEMPLATE as a single file name "
    "component,\n"
    "                         relative to a directory: $TMPDIR, if set; else "
    "the\n"
    "                         directory specified via -p; else " _PATH_TMP
    " [deprecated]\n\n"
    "      --help             display this help and exit\n"
    "      --version          output version information and exit";

#define X_CHR	   ML99_CHAR_LIT(X)
#define X_STR	   ML99_STRINGIFY(X)
#define MIN_XS	   3 /* minimum nuber of Xs */
#define XS(...)	   ML99_EVAL(ML99_times(v(MIN_XS), v(X_STR)))
#define RANDOM_DEV _PATH_DEV "urandom"

/* max attempts to find the name of a non-existent file */
#define MAX_RETRIES 100

#define PRINTF_ATTR __attribute__((__format__(printf, 1, 2)))

static inline _Noreturn void fatal(const char* fmt, ...) PRINTF_ATTR;
static inline _Noreturn void fatalx(const char* fmt, ...) PRINTF_ATTR;
static inline char*	     get_tmp(void);
static inline void	     eclose(int fd, const char* path);

static char* my_mktemp(const char* template, const char* suffix);

static char DEFAULT_TEMPLATE[] = "tmp.XXXXXXXXXX";

static enum flag qflag	       = NO;

/* in letters.c */
extern const char	  LETTERS[];
extern const unsigned int LETTERS_LEN;

int
main(int argc, char* argv[])
{
	enum flag INIT_FLAGS(NO, d, t, u), need_free = NO;

	char* template, *suffix = NULL, *prefix = NULL;

	int ch;
	while ((ch = getopt_long(argc, argv, "dp:qtuV", LONG_OPTIONS, NULL))
	       != -1) {
		switch (ch) {
			IS_FLAGS(YES, d, q, u);

		case 'p':
			prefix = optarg;
			/* fallthrough */

		case 't':
			tflag = YES;
			break;

		case SUFFIX_OPTION:
			suffix = optarg;
			break;

		case 'V': /* fallthrough */
			GETOPT_END_CASE
		}
	}

	if (*(argv += optind)) {
		template = *argv;
		if (*++argv) errx(!0, "extra operand: %s", *argv);
	} else {
		tflag	 = YES;
		template = DEFAULT_TEMPLATE;
	}

	size_t len    = strlen(template);
	char*  last_x = strrchr(template, X_CHR);
	if (!last_x || (last_x - template) < MIN_XS
	    || !STRNEQ(&template[last_x - template - MIN_XS + 1],
		       XS(),
		       MIN_XS))
		fatalx(
		    "insufficient number of Xs in template '%s'"
		    " (required at least %u)",
		    template,
		    MIN_XS);

	if (suffix && strchr(suffix, '/'))
		fatalx("suffix must not contain directory separators");

	if (tflag) {
		if (strchr(template, '/'))
			fatalx(
			    "template must not contain directory separators "
			    "in -t mode");

		if (!prefix) prefix = get_tmp();
		__typeof__(len) prefix_len = strlen(prefix);
		while (prefix_len && prefix[prefix_len - 1] == '/')
			prefix_len--;

		if (asprintf(&template,
			     "%.*s/%.*s",
			     (int)prefix_len,
			     prefix,
			     (int)len,
			     template)
		    == -1)
			fatal("failed to allocate memory for template");

		need_free = YES;
	}

	char* path;
	if (!(path = my_mktemp(template, suffix))) {
	err:
		fatal("failed to make temp %s for template '%s%s'",
		      dflag ? "dir" : "file",
		      template,
		      suffix ? suffix : "");
	}

	if (!uflag) {
		if (dflag) {
			if (mkdir(path, 0700)) goto err;
		} else {
			int fd;
			if ((fd = open(path,
				       O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC,
				       0600))
			    < 0)
				goto err;
			else
				eclose(fd, path);
		}
	}

	puts(path);

	if (need_free) free(template);
	free(path);

	return 0;
}

static inline _Noreturn void
fatal(const char* fmt, ...)
{
	if (!qflag) {
		va_list ap;

		va_start(ap, fmt);
		vwarn(fmt, ap);
		va_end(ap);
	}

	exit(!0);
}

static inline _Noreturn void
fatalx(const char* fmt, ...)
{
	if (!qflag) {
		va_list ap;

		va_start(ap, fmt);
		vwarnx(fmt, ap);
		va_end(ap);
	}

	exit(!0);
}

static inline char*
get_tmp(void)
{
	char* tmp;
	char* var[] = { "TMPDIR", "TMP", NULL };

	for (char** ptr = var; *ptr; ++ptr) {
		if (!!(tmp = getenv(*ptr))) return tmp;
	}

	return _PATH_TMP;
}

static inline enum flag
fill_dev_random_str(char* dst, size_t len)
{
	int fd;

	if ((fd = open(RANDOM_DEV, O_RDONLY | O_LARGEFILE | O_CLOEXEC))
	    == -1) {
		if (!qflag) warn("failed to open '%s'", RANDOM_DEV);
		return NO;
	}

	enum flag ret;
	if (!(ret = read(fd, dst, len) == (ssize_t)len)) {
		/* read random bytes to dst */
		if (!qflag)
			warn("failed to read %lu bytes from '%s'",
			     len,
			     RANDOM_DEV);
		return NO;
	}

	eclose(fd, RANDOM_DEV);

	for (size_t i = 0; i < len; ++i)
		/* convert random bytes to random letters */
		dst[i] = LETTERS[(uint8_t)dst[i] % LETTERS_LEN];

	return ret;
}

static inline void
fill_rand_str(char* dst, size_t len)
/*
 * try to use RANDOM_DEV or if failed use OS independent random generator
 */
{
	if (fill_dev_random_str(dst, len)) return;

	uint32_t num = 0;
	uint8_t* n;

	static enum flag seed_initialized = NO;

	if (!seed_initialized) {
		unsigned int seed = 69;

		struct timeval tm;
		gettimeofday(&tm, NULL);

		SAFE_ASSIGN(seed,
			    tm.tv_sec
				+ tm.tv_usec * (__typeof__(tm.tv_usec))&tm);
		srandom(seed);

		seed_initialized = YES;
	}

	for (size_t i = 0; i < len;) {
		SAFE_ASSIGN(num, random());
		SAFE_ASSIGN(n, &num);

		/* in `u32` fits 4 `u8` */
		for (size_t j = 0; j < (sizeof(num) / sizeof(*n)) && i < len;
		     ++j, ++i)
			dst[i] = LETTERS[n[j] % LETTERS_LEN];
	}
}

static inline enum flag
path_exists(const char* path)
{
	struct stat statbuf;
	if (!lstat(path, &statbuf) || errno == EOVERFLOW) errno = EEXIST;

	return errno != ENOENT;
}

static char*
my_mktemp(const char* template, const char* suffix)
{
	char* ret;

	const char* last_x = strrchr(template, X_CHR);

	assert(last_x);

	/* from begin to last X */
	size_t to_last_x    = (size_t)(last_x - template);
	size_t template_len = strlen(template);
	size_t suffix_len   = suffix ? strlen(suffix) : 0;

	char path[template_len + suffix_len + (1 /* 0 */)];

	memcpy(path, template, template_len);
	memcpy(path + template_len, suffix, suffix_len);
	path[LEN(path) - 1] = '\0';

	{
		size_t xs = 0;

		for (char* ptr = (char*)template + to_last_x;
		     ptr >= template && *ptr == X_CHR;
		     --ptr)
			xs++;

		assert(xs >= MIN_XS);

		size_t i = 0;
		do {
			fill_rand_str(path + to_last_x - xs + 1, xs);
			++i;
		} while (path_exists(path) && i < MAX_RETRIES);
		if (i == MAX_RETRIES) return NULL;
	}

	if (!!(ret = strdup(path))) return ret;

	fatal("failed to allocate memory for path");
}

static inline void
eclose(int fd, const char* path)
{
	if (!!close(fd) && !qflag)
		warn("failed to close '%s' (fd: %lu)", path, fd);
}
